'use client';
import type { Components } from 'hast-util-to-jsx-runtime';
import { Check, Copy } from 'lucide-react';
import {
  type ComponentProps,
  forwardRef,
  type HTMLAttributes,
  type MouseEventHandler,
  type ReactNode,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import type {
  Awaitable,
  BundledLanguage,
  BundledTheme,
  CodeOptionsMeta,
  CodeOptionsThemes,
  CodeToHastOptionsCommon,
  RegexEngine,
} from 'shiki';

import { buttonVariants } from './button';
import { useShiki } from '../lib/use-shiki';
import { cn } from '../lib/utils';

export function useEffectEvent<F extends (...params: never[]) => unknown>(callback: F): F {
  const ref = useRef(callback);
  ref.current = callback;

  return useCallback(((...params) => ref.current(...params)) as F, []);
}

export function useCopyButton(onCopy: () => void | Promise<void>): [checked: boolean, onClick: MouseEventHandler] {
  const [checked, setChecked] = useState(false);
  const timeoutRef = useRef<number | null>(null);

  const onClick: MouseEventHandler = useEffectEvent(() => {
    if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
    const res = Promise.resolve(onCopy());

    // biome-ignore lint/complexity/noVoid: part of clipboard implementation
    void res.then(() => {
      setChecked(true);
      timeoutRef.current = window.setTimeout(() => {
        setChecked(false);
      }, 1500);
    });
  });

  // Avoid updates after being unmounted
  useEffect(() => {
    return () => {
      if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
    };
  }, []);

  return [checked, onClick];
}

type HighlightOptionsThemes = CodeOptionsThemes<BundledTheme>;
type HighlightOptionsCommon = CodeToHastOptionsCommon<BundledLanguage> &
  CodeOptionsMeta & {
    engine?: Awaitable<RegexEngine>;
    components?: Partial<Components>;
  };

export type CodeBlockProps = HTMLAttributes<HTMLElement> & {
  /**
   * Icon of code block
   *
   * When passed as a string, it assumes the value is the HTML of icon
   */
  icon?: ReactNode;

  /**
   * Allow to copy code with copy button
   *
   * @defaultValue true
   */
  allowCopy?: boolean;

  /**
   * Keep original background color generated by Shiki or Rehype Code
   *
   * @defaultValue false
   */
  keepBackground?: boolean;

  viewportProps?: HTMLAttributes<HTMLElement>;

  /**
   * show line numbers
   */
  'data-line-numbers'?: boolean;

  /**
   * @defaultValue 1
   */
  'data-line-numbers-start'?: number;

  /**
   * Test identifier for the component
   */
  testId?: string;
};

export const Pre = forwardRef<HTMLPreElement, HTMLAttributes<HTMLPreElement>>(({ className, ...props }, ref) => {
  return (
    <pre ref={ref} className={cn('min-w-full w-max *:flex *:flex-col', className)} {...props}>
      {props.children}
    </pre>
  );
});

Pre.displayName = 'Pre';

export const CodeBlock = forwardRef<HTMLElement, CodeBlockProps>(
  ({ title, allowCopy = true, keepBackground = false, icon, viewportProps, children, testId, ...props }, ref) => {
    const areaRef = useRef<HTMLDivElement>(null);
    const onCopy = () => {
      const pre = areaRef.current?.getElementsByTagName('pre').item(0);
      if (!pre) return;

      const clone = pre.cloneNode(true) as HTMLElement;
      for (const node of Array.from(clone.querySelectorAll('.nd-copy-ignore'))) {
        node.replaceWith('\n');
      }

      // biome-ignore lint/complexity/noVoid: part of clipboard implementation
      void navigator.clipboard.writeText(clone.textContent ?? '');
    };

    return (
      <figure
        ref={ref}
        dir="ltr"
        {...props}
        data-testid={testId}
        className={cn(
          'not-prose group relative my-4 overflow-hidden rounded-lg border bg-fd-card text-sm outline-none',
          keepBackground && 'bg-(--shiki-light-bg) dark:bg-(--shiki-dark-bg)',
          props.className,
        )}
      >
        {title ? (
          <div className="flex items-center gap-2 bg-fd-secondary px-4 py-1.5">
            {icon ? (
              <div
                className="text-fd-muted-foreground [&_svg]:size-3.5"
                // biome-ignore lint/security/noDangerouslySetInnerHtml: part of DynamicCodeBlock implementation
                // biome-ignore lint/security/noDangerouslySetInnerHtmlWithChildren: part of DynamicCodeBlock implementation
                dangerouslySetInnerHTML={
                  typeof icon === 'string'
                    ? {
                        __html: icon,
                      }
                    : undefined
                }
              >
                {typeof icon !== 'string' ? icon : null}
              </div>
            ) : null}
            <figcaption className="flex-1 truncate text-fd-muted-foreground">{title}</figcaption>
            {allowCopy && <CopyButton className="-me-2" onCopy={onCopy} />}
          </div>
        ) : (
          allowCopy && <CopyButton className="absolute right-2 top-2 z-[2] backdrop-blur-md" onCopy={onCopy} />
        )}
        <div
          ref={areaRef}
          {...viewportProps}
          className={cn(
            'text-[13px] py-3.5 overflow-auto [&_.line]:px-4 max-h-[600px] fd-scroll-container',
            props['data-line-numbers'] && '[&_.line]:pl-3',
            viewportProps?.className,
          )}
          style={{
            counterSet: props['data-line-numbers']
              ? `line ${Number(props['data-line-numbers-start'] ?? 1) - 1}`
              : undefined,
            ...viewportProps?.style,
          }}
        >
          {children}
        </div>
      </figure>
    );
  },
);

CodeBlock.displayName = 'CodeBlock';

function CopyButton({
  className,
  onCopy,
  ...props
}: ComponentProps<'button'> & {
  onCopy: () => void;
}) {
  const [checked, onClick] = useCopyButton(onCopy);

  return (
    <button
      type="button"
      className={cn(
        buttonVariants({
          variant: 'ghost',
        }),
        'transition-opacity group-hover:opacity-100 [&_svg]:size-3.5',
        !checked && '[@media(hover:hover)]:opacity-0',
        className,
      )}
      aria-label={checked ? 'Copied Text' : 'Copy Text'}
      onClick={onClick}
      {...props}
    >
      <Check className={cn('transition-transform', !checked && 'scale-0')} />
      <Copy className={cn('absolute transition-transform', checked && 'scale-0')} />
    </button>
  );
}

function pre(props: ComponentProps<'pre'> & { testId?: string }) {
  return (
    <CodeBlock {...props} className={cn('my-0', props.className)} testId={props.testId}>
      <Pre>{props.children}</Pre>
    </CodeBlock>
  );
}

export function DynamicCodeBlock({
  lang,
  code,
  options,
  testId,
}: {
  lang: string;
  code: string;
  options?: Omit<HighlightOptionsCommon, 'lang'> & HighlightOptionsThemes;
  testId?: string;
}) {
  const components: HighlightOptionsCommon['components'] = {
    pre: (props: ComponentProps<'pre'>) => pre({ ...props, testId }),
    ...options?.components,
  };

  // biome-ignore lint/correctness/useExhaustiveDependencies: initial value only
  const loading = useMemo(() => {
    const Pre = (components.pre ?? 'pre') as 'pre';
    const Code = (components.code ?? 'code') as 'code';

    return (
      <Pre>
        <Code>
          {code.split('\n').map((line, i) => (
            // biome-ignore lint/suspicious/noArrayIndexKey: part of DynamicCodeBlock implementation
            <span key={i} className="line">
              {line}
            </span>
          ))}
        </Code>
      </Pre>
    );
  }, []);

  return useShiki(code, {
    lang,
    loading,
    withPrerenderScript: true,
    ...options,
    components,
  });
}
